using System;
using System.Collections.Generic;
using System.Text;
using Sedna.Domain.Enum;
using Sedna.Util;

namespace Sedna.Domain.GenericRange
{
    /// <summary>
    /// Since <see cref="DateTime"/> is a structure and must be initialized then
    /// StartDate be treated as not set if it equals to <see cref="DateTime.MinValue"/> and
    /// EndDate be treated as not set if it equals to <see cref="DateTime.MaxValue"/>
    /// </summary>
    public class DateRange : Range<DateTime>
    {
        // DateShortFormat
        
        public static readonly DateRange EMPTY = new DateRange(DateTime.MaxValue, DateTime.MinValue);

        private DateTimePrecision precision;

        # region Constructors

        public DateRange(DateTime start, DateTime end)
            : this(start, end, DateTimePrecision.ToDay)
        {
        }

        public DateRange(DateTime start, DateTime end, DateTimePrecision precision)
            : base(start, end)
        {
            this.precision = precision;
        }

        # endregion

        # region Factory Methods

        public static DateRange UpTo(DateTime end)
        {
            return new DateRange(DateTime.MinValue, end);
        }

        public static DateRange StartingOn(DateTime start)
        {
            return new DateRange(start, DateTime.MaxValue);
        }

        # endregion

        # region Properties

        public DateTimePrecision Precision
        {
            get { return precision; }
            set { precision = value; }
        }

        # endregion

        public TimeSpan Difference()
        {
            return Difference(precision);
        }

        /// <summary>
        /// As far [Start = End] is not an empty range then
        /// is this case difference will be greater than zero.
        /// </summary>
        public TimeSpan Difference(DateTimePrecision precision)
        {
            DateTimePrecision currPrecision = this.precision;

            // To perform internal operations with specified precision
            this.precision = precision;

            try
            {
                if (IsEmpty)
                    return new TimeSpan();

                return DateUtility.Add(DateUtility.Convert(End, precision), 1, precision) - DateUtility.Convert(Start, precision);
            }
            finally
            {
                this.precision = currPrecision;
            }
        }

        public DateRange Gap(Range<DateTime> range, DateTimePrecision precision)
        {
            if (range == null)
                throw new ArgumentNullException("Range cannot be null");

            // To perform internal operations with specified precision
            
            DateTimePrecision thisCurrPrecision = this.precision;
            DateTimePrecision thatCurrPrecision = precision; // just to initialize
            
            if (range is DateRange)
            {
                thatCurrPrecision = (range as DateRange).Precision;
                (range as DateRange).Precision = precision;
            }
            
            this.precision = precision;

            try
            {
                if (Overlaps(range)) return EMPTY;

                Range<DateTime> lower, higher;

                if (CompareTo(range) < 0)
                {
                    lower = this;
                    higher = range;
                }
                else
                {
                    lower = range;
                    higher = this;
                }

                return EvalGap(lower, higher, precision);
            }
            finally
            {
                this.precision = thisCurrPrecision;

                if (range is DateRange)
                {
                    (range as DateRange).Precision = thatCurrPrecision;
                }
            }
        }

        protected override Range<DateTime> EvalGap(Range<DateTime> lower, Range<DateTime> higher)
        {
            return EvalGap(lower, higher, precision);
        }

        protected DateRange EvalGap(Range<DateTime> lower, Range<DateTime> higher, DateTimePrecision precision)
        {
            return new DateRange(DateUtility.Add(lower.End, 1, precision), DateUtility.Add(higher.Start, -1, precision), precision);
        }

        public override int GetHashCode()
        {
            if (IsEmpty)
                return EMPTY.Start.GetHashCode();

            return Start.GetHashCode();
        }


        protected override bool Equals(DateTime t1, DateTime t2)
        {
            return DateUtility.IsEqual(t1, t2, precision);
        }

        protected override int Compare(DateTime t1, DateTime t2)
        {
            return DateUtility.Compare(t1, t2, precision);
        }

        /// <summary>
        /// Treated as not set if it equals to <see cref="DateTime.MinValue"/> 
        /// </summary>
        protected override bool IsStartInitialized
        {
            get { return Start.CompareTo(DateTime.MinValue) > 0; }
        }

        /// <summary>
        /// Treated as not set if it equals to <see cref="DateTime.MaxValue"/>
        /// </summary>
        protected override bool IsEndInitialized
        {
            get { return End.CompareTo(DateTime.MaxValue) < 0; }
        }

        protected override bool IsTypeValid(Range<DateTime> range)
        {
            if (range == null)
                throw new ArgumentNullException("Range cannot be null");
            
            return range is DateRange;
        }

        protected override Range<DateTime> EvalEmpty
        {
            get { return EMPTY; }
        }               
    }
}
